PNS(Polkadot Name Service) 是一个建立在 Polkadot 上的域名系统,它的主要功能是域名解析,即将一个例如 “polka.dot” 这样一个可读性和可记忆性都非常好的字符串翻译成 Polkadot 上一长串无实际意义的地址。
这样我们就可以在转账、投票以及一些 dapp 操作中使用像 “polka.dot” 这样简单易懂的『域名』而不是冗长难记的『地址』。就好像现实生活中我们我们访问网站使用的是例如 “google.com” 的『域名』,而不是谷歌机房的 ip 地址。
将 “google.com” 翻译成谷歌主机 ip 的服务就是 DNS(Domain Name Service),而目前全球的 IPV4 根域名服务器只有13台,其中9台在美国,2台在欧洲,1台在亚洲,如此中心化的分布也导致了互联网上有一个说法:攻击整个因特网最有力、最直接,也是最致命的方法恐怕就是攻击根域名服务器了。
而相比于 DNS,PNS 由于直接架构在 Polkadot 上,因此天然的拥有去中心化的特点,所以传统的攻击根域名服务器的方法自然无法奏效。
除了基础的域名解析服务,PNS还提供了安全可靠的域名注册、拍卖、转让以及交易等功能。
域名解析
域名注册
eth-ens-namehash 这个 javascript 库提供了
和 hash
方法,对域名进行前置处理,使用 normalize
来对域名进行标准化处理虽然支持UTS46
编码的字符,但是同时也导致了一些钓鱼域名可以注册成功。例如 faceboоk.eth 和 facebook.eth 看起来似乎是两个同样的字符串,但是却都可以在 ENS 上注册成功,这是因为第一个 facebook 中的第二个 ο 是其实希腊字母 Ομικρον ,只是看起来一样罢了,而如果允许这样的情况继续发生的话,那么在现代互联网中屡见不鲜的『同形异义字』的钓鱼域名攻击在区块链中依然无法幸免。utf-8
所以在 PNS 的域名规则里我们只允许这些字符:
。虽然这样会有不尊重少数语言的嫌疑,但是为了表面意义上的政治正确而增加用户的资产风险显然是个更加错误的决定。.abcdefghijklmnopqrstuvwxyz1234567890
这个字符严格意义上并不属于 PNS 域名规则中可以使用的字符,但是它确实会出现在域名中,例如:"polka.dot" 和 "chainx.polka.dot",从前面两个例子可以看出来,这里的.
.
和我们常见域名的作用是一样的,即用来区分域名层级而并没有实际的含义。
域名长度
短域名(3-6个字符,需要拍卖,示例:chainx.dot) 长域名(7-12字符,支付租用费选择租用期限并注册,示例:chainxpool.dot)
注册步骤
填写想要注册的域名(如 chainx) 选择域名时效(默认1年有效期,可续期,租用费用和租用市场相关,大于3年可给予一定优惠) 支付费用提交交易,交易成功后获取域名 可选:默认绑定交易地址,可更改绑定地址
拍卖方式
英式拍卖,以一年期租用费用为起拍价,无保留价 拍卖系统定期放出一部分短域名进行竞拍,在规定期限内,首次出价最高的用户将会获得域名。
如无人竞拍,域名将以起拍价放置于代理交易系统,任何想获得该域名的用户都可以通过代理交易方式获取该域名。
拍卖时长
5-6 个字符,4周 4 个字符,5周 3个字符,6周
域名属性
代理交易
Bob 没有及时完成购买操作,域名被其他人买了 Alice 在挂出之后就及时通知 Bob 进行购买,但是依然会被一些自动脚本或者恶意抢注的人先行一步的购买成功
出价转让
在几乎所有的区块链应用中都会强调例如去中心化、匿名、安全等关键字,但是对于真正需要交互的区块链应用来说,匿名或许并不是一个值得称道的点。比如在域名的转让过程中,不可能第一次出价就能够让双方都满意,那么彼此的讨价还价就显得很有必要了。在智能合约里讨价还价技术上确实是可行的,但是实际上是一种为了区块链而区块链的浪费资源且耽误时间的行为。因此如果我们可以将用户的联系方式(Email)作为域名的一个属性(如果能够切实的对用户提供便利,那么用户可能并不介意填写自己的电子邮箱),那么毫无关联的两个用户完全可以通过更高效的方式完成域名价格的确定,然后再通过 PNS 提供的『代理交易合约』来安全的完成域名交易,这样既兼顾用户体验又确保交易安全性的交互方式或许更加符合大部分用户的真实需求。
域名管理
更改映射地址 添加子域名 更改owner renew
合约实现
创建合约
cargo contract new simple-pns
,新建一个合约项目。定义合约结构
struct SimplePns {
/// A hashmap to store all name to addresses mapping
name_to_address: storage::HashMap<Hash, AccountId>,
/// A hashmap to store all name to owners mapping
name_to_owner: storage::HashMap<Hash, AccountId>,
default_address: storage::Value<AccountId>,
}
其中 name_to_address
是一个存储域名到映射地址的 hashmap,name_to_owner
是一个存储域名到域名所有者的 hashmap,default_address
是一个类型为 AccountId
的空地址。
初始化合约
impl Deploy for SimplePns {
/// Initializes contract with default address.
fn deploy(&mut self) {
self.default_address.set(AccountId::from([0x0; 32]));
}
}
实现域名操作方法
impl SimplePns {
/// Register specific name with caller as owner
pub(external) fn register(&mut self, name: Hash) -> bool {
let caller = env.caller();
if self.is_name_exist_impl(name) {
return false
}
env.println(&format!("register name: {:?}, owner: {:?}", name, caller));
self.name_to_owner.insert(name, caller);
env.emit(Register {
name: name,
from: caller,
});
true
}
/// Set address for specific name
pub(external) fn set_address(&mut self, name: Hash, address: AccountId) -> bool {
let caller: AccountId = env.caller();
let owner: AccountId = self.get_owner_or_none(name);
env.println(&format!("set_address caller: {:?}, owner: {:?}", caller, owner));
if caller != owner {
return false
}
let old_address = self.name_to_address.insert(name, address);
env.emit(SetAddress {
name: name,
from: caller,
old_address: old_address,
new_address: address,
});
return true
}
/// Transfer owner to another address
pub(external) fn transfer(&mut self, name: Hash, to: AccountId) -> bool {
let caller: AccountId = env.caller();
let owner: AccountId = self.get_owner_or_none(name);
env.println(&format!("transfer caller: {:?}, owner: {:?}", caller, owner));
if caller != owner {
return false
}
let old_owner = self.name_to_owner.insert(name, to);
env.emit(Transfer {
name: name,
from: caller,
old_owner: old_owner,
new_owner: to,
});
return true
}
/// Get address for the specific name
pub(external) fn get_address(&self, name: Hash) -> AccountId {
let address: AccountId = self.get_address_or_none(name);
env.println(&format!("get_address name is {:?}, address is {:?}", name, address));
address
}
/// Check whether name is exist
pub(external) fn is_name_exist(&self, name: Hash) -> bool {
self.is_name_exist_impl(name)
}
}
/// Implement some private methods
impl SimplePns {
/// Returns an AccountId or default 0x00*32 if it is not set.
fn get_address_or_none(&self, name: Hash) -> AccountId {
let address = self.name_to_address.get(&name).unwrap_or(&self.default_address);
*address
}
/// Returns an AccountId or default 0x00*32 if it is not set.
fn get_owner_or_none(&self, name: Hash) -> AccountId {
let owner = self.name_to_owner.get(&name).unwrap_or(&self.default_address);
*owner
}
/// check whether name is exist
fn is_name_exist_impl(&self, name: Hash) -> bool {
let address = self.name_to_owner.get(&name);
if let None = address {
return false;
}
true
}
}
可以看到在上面具体的方法中我们使用 env.emit
触发的一些事件,所以我们还需要定义这些事件:
name: Hash,
from: AccountId,
}
event SetAddress {
name: Hash,
from: AccountId,
old_address: Option<AccountId>,
new_address: AccountId,
}
event Transfer {
name: Hash,
from: AccountId,
old_owner: Option<AccountId>,
new_owner: AccountId,
}
编写测试函数
#[cfg(all(test, feature = "test-env"))]
mod tests {
use super::*;
use ink_core::env;
type Types = ink_core::env::DefaultSrmlTypes;
#[test]
fn register_works() {
let alice = AccountId::from([0x1; 32]);
// let bob: AccountId = AccountId::from([0x2; 32]);
let name = Hash::from([0x99; 32]);
let mut contract = SimplePns::deploy_mock();
env::test::set_caller::<Types>(alice);
assert_eq!(contract.register(name), true);
assert_eq!(contract.register(name), false);
}
#[test]
fn set_address_works() {
let alice = AccountId::from([0x1; 32]);
let bob: AccountId = AccountId::from([0x2; 32]);
let name = Hash::from([0x99; 32]);
let mut contract = SimplePns::deploy_mock();
env::test::set_caller::<Types>(alice);
assert_eq!(contract.register(name), true);
// caller is not owner, set_address will be failed
env::test::set_caller::<Types>(bob);
assert_eq!(contract.set_address(name, bob), false);
// caller is owner, set_address will be successful
env::test::set_caller::<Types>(alice);
assert_eq!(contract.set_address(name, bob), true);
assert_eq!(contract.get_address(name), bob);
}
#[test]
fn transfer_works() {
let alice = AccountId::from([0x1; 32]);
let bob = AccountId::from([0x2; 32]);
let name = Hash::from([0x99; 32]);
let mut contract = SimplePns::deploy_mock();
env::test::set_caller::<Types>(alice);
assert_eq!(contract.register(name), true);
// transfer owner
assert_eq!(contract.transfer(name, bob), true);
// now owner is bob, alice set_address will be failed
assert_eq!(contract.set_address(name, bob), false);
env::test::set_caller::<Types>(bob);
// now owner is bob, set_address will be successful
assert_eq!(contract.set_address(name, bob), true);
assert_eq!(contract.get_address(name), bob);
}
}
运行测试
cargo +nightly test
来测试合约函数,如果得到下面的结果,证明测试通过。cargo contract build
编译合约,并使用命令 cargo +nightly build --features ink-generate-abi
编译 ABI。target
目录下会出现相应的 wasm
和 json
文件。部署合约
substrate --dev
在本地启动一个 substrate 节点,然后克隆 polkadot-app 到本地,并连接到本地节点。contracts
页面上传相应的文件。上传成功之后,我们还需要部署合约:
然后按照下图输入相应的数值,点击部署:
部署成功后,就可以调用合约的具体函数了,由于目前 ink 以及相关的工具链还不是很完善,想要验证数据只能在合约中使用 env.println
来在 substrate
节点的控制台中输出相关信息。
注意:
env.println
只在substrate --dev
模式下有效
现在让我们测试一下注册域名能否成功吧~
调用 register
函数:
可以看到控制台中的 name
对应 0x9e9de23f4d89d086c74c9fa23e4f4ceff6f9b68165b60b70290d1e5820f4bf4d
,调用成功!